# 本节介绍以下内容：1.访问参数，用于调试、诊断和可视化；2.参数初始化；3.在不同模型组件间共享参数
import torch
from torch import nn

# 单隐藏层的感知机
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)

# 访问参数
# 当通过Sequential类定义模型时，我们可以通过索引来访问模型的任意层。模型是一个列表一样，每层的参数都在其属性中。
# 如下所示，我们可以检查第二个全连接层的参数。(这里用state_dict函数实现)(relu也是一层)
print(net[2].state_dict())
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
net[2].weight.grad == None  # 还可以访问参数的梯度，由于我们还没有调用反向传播，所以参数的梯度处于初始状态

# 一次性访问所有参数
# 访问第一个全连接层的参数和访问所有层
print(*[(name, param.shape) for name, param in net[0].named_parameters()])  # 注意指针所以要*号，注意用named_parameters函数
print(*[(name, param.shape) for name, param in net.named_parameters()])
# 根据以上的name输出提供了另一种访问参数的方式
net.state_dict()['2.bias'].data

# 多个块嵌套的参数如何命名的，让我们看看，如果我们将多个块相互嵌套，参数命名约定是如何工作的。
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add_module(f'block {i}', block1())
    return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
# 设计了网络后，我们看看它是如何工作的
print(rgnet)  # print网络可以输出他的模型架构
# 因为层是分层嵌套的，所以我们也可以像通过嵌套列表索引一样访问它们。下面，我们访问第一个主要的块中、第二个子块的第一层的偏置项。
rgnet[0][1][0].bias.data

# 参数初始化，默认情况下，PyTorch会根据一个范围均匀地初始化权重和偏置矩阵，这个范围是根据输入和输出维度计算出的。PyTorch的nn.init模块提供了多种预置初始化方法
# 内置初始化：让我们首先调用内置的初始化器。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量，且将 偏置参数设置为0
def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)  # 使用方式net.apply，注意传入的参数函数不用括号
net[0].weight.data, net[0].bias.data  # 也可以指定输出weight的第几行net[0].weight.data[0]
# 还可以将所有参数初始化为给定的常数，比如初始化为1。
def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)  # 使用方式net.apply，注意传入的参数函数不用括号
net[0].weight.data, net[0].bias.data

# 对某些块应用不同的初始化方法，下面我们使用Xavier初始化方法初始化第一个神经网络 层，然后将第三个神经网络层初始化为常量值42
def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_normal_(m.weight)  # Xavier初始化是在标准初始化方法的基础上，兼顾了各层在前向传播和分享传播时的参数方差
        nn.init.zeros_(m.bias)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)
        nn.init.zeros_(m.bias)
net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

# 自定义初始化，有时，深度学习框架没有提供我们需要的初始化方法。在下面的例子中，我们使用以下的分布为任意权重参数w定义初始化方法：
# U(5, 10)的可能性=1/4; 0的可能性=1/2；U(−10, −5)的可能性=1/4
def my_init(m):
    if type(m) == nn.Linear:
        # 访问所有层参数的[0]即weight，注意这里的循环是m的named_parammeters，而不是net的
        print("Init", *[(name, param.shape) for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight
# 注意，我们始终可以直接设置参数
net[0].weight.data += 1
net[0].weight.data[0][0] = 42
net[0].weight.data

# 参数绑定，有时我们希望在多个层间共享参数：我们可以定义一个稠密层，然后使用它的参数来设置另一个层的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(f'相同为True，否则为False ', net[2].weight.data == net[4].weight.data)
# 看看运算是否会同步更新
net[0].weight.data[0] = 100
print(f'相同为True，否则为False ', net[2].weight.data == net[4].weight.data)
# 这个例子表明第三个和第五个神经网络层的参数是绑定的。它们不仅值相等，而且由相同的张量表示。因此，如果我们改变其中一个参数，另一个参数也会改变。
# 这里有一个问题：当参数绑定时，梯度会发生什么情况？ 答案是由于模型参数包含梯度，因此在反向传播期间第二个隐藏层（即第三个神经网络层）和第三个隐藏层 （即第五个神经网络层）的梯度会加在一起。